package com.atguigu.controller;

import com.atguigu.bean.Book;
import org.springframework.dao.support.DaoSupport;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;

/**
 * 测试：@ModelAttribute注解
 *      作用：
 *           【方法入参】标注该注解，入参的对象就会放到数据模型中
 *           在【方法上】标注注解，该方法就会在目标方法前，先执行
 *
 * 使用场景：
 *      书城的图书修改：sql语句是全字段修改；但是前端页面给的数据不完整，有些数据是null，
 *      这时的全字段修改的sql，就会将前端是null的值，也会填入数据库，这非常的危险！【使用MyBatis不会有这个问题】
 *
 * 场景复现：
 *      1）、页面端：
 *              显示要修改的图书信息，图书的所有字段都显示
 *
 *              名称  价格  作者  销量  库存  操作
 *              西游记 99   吴承恩 123  100  修改
 *
 *      2）、Servlet收到修改请求后，调用dao修改数据：本类的updateBook()方法
 *              String sql = "updated bs_book set title=?,
 *                              author=?,price=?,sales=?,
 *                              stock=?,img_path=?,
 *                              where id=?";
 *
 *      3）、在实际场景中，并不是全字段修改，只会修改部分字段，
 *          例如：书名就不可以更改，
 *          再例如 账号密码的修改，就会禁止用户，修改账号！
 *          所以：
 *              （1）不修改的字段，可以在页面进行展示，但是不能提供修改框
 *                  src/main/webapp/index.jsp
 *              （2）为了简单，在方法的入参处，直接填入Book对象，由SpringMVC自动为我们封装Book对象（前端没有传递的值是null）
 *              （3）接下来dao调用了全字段更新的sql，会将book对象中的null值也提交到数据库修改，那么数据库的数据就被破坏了！！！
 *              （4）问题的根本：
 *                      sql是全字段更新！！
 *
 *                      所以在sql处，加入判断操作：
 *                          sql = "update bs_book set"
 *                          if(book.getBookName()){
 *                              sql += "bookName = ?，";
 *                          }if(book.getPrice()){
 *                              sql += "price = ?，";
 *                          }
 *
 *                 拼接sql，非常的麻烦，还要处理sql语句中的逗号“，”问题【不推荐改sql】
 *
 *          ★★★如何能保证全字段更新的时候，只更新页面携带的数据？？？
 *              1）、修改dao，代价太大【不推荐】
 *              2）、在本类的updateBook()方法，在Book对象封装之前，将Book对象内的bookName，提前在数据库中，获取出来，存入Book对象内，
 *                  再继续让SpringMVC封装Book对象。
 *
 *                      问题：
 *                      1）、Book对象是如何封装的？【★★★这就是null的原因！】
 *                          （1）SpringMVC创建Book对象，每个属性都有默认值，bookName默认就是null【★★★这就是null的原因！】
 *                          （2）SpringMVC将Book对象的所有属性，在request域中，都get一次，得到数据，封装在Book对象中。
 *
 *                          解决方法：
 *                              1）、让SpringMVC别创建Book对象，而是从数据库中取出bookId=100的对象：
 *                              bookI=100, bookName='西游记', author='吴承恩', price=99.98, stock=100, sales=123
 *
 *                              2）、让SpringMVC使用，刚才在数据库中取出的book对象，给它设置值。
 *                              那么，前端页面携带的值，就会被更新；没有携带的值，就会保持原来的值。再调用全字段更新，就会没有问题。
 *
 *                      2）、null值，如何而来？
 *                            SpringMVC封装Book对象的时候，没有get到的数据，也就是请求中，没有携带的数据，
 *                            就不会被设置进来，那么null值就是，SpringMVC在创建Book对象的时候，java规定的默认值！！！
 *
 *                      3）、接下来，调用全字段更新就会有问题！
 *
 *
 *  在SpringMVC封装Book对象前，提前从数据库获取Book对象，
 *  将此Book对象交给SpringMVC封装数据，而不是重新new一个新的对象。使用的注解就是@ModelAttribute
 *      1）、SpringMVC要封装请求参数的Book对象，不应该是自己new出来，而是【从数据库中】拿到准备好的对象
 *      2）、再来使用这个对象封装，请求参数
 *
 *  注解@ModelAttribute
 *      使用位置：
 *          方法：这个方法就会提前于目标方法先运行，将数据存储在BindingAwareModelMap中。
 *                  【目标方法是updateBook()，要先于updateBook()方法之前，将Book对象从数据库中获取出来】
 *          参数：取出BindingAwareModelMap中，保存的数据
 *
 *  测试方法：
 *      hahaMyModelAttribute()，可以在此方法内，提前查出数据库中的，book对象【方法上使用@ModelAttribute】
 *          1、从数据库中获取，图书信息
 *          2、将这个book对象，保存起来，方便目标方法使用
 *             1）在方法的入参处，传递Map、Model、ModelMap
 *             2）将book对象，保存在Map、Model、ModelMap中
 *
 *      updateBook()【入参处使用@ModelAttribute】
 *          3、告知SpringMVC，目标方法updateBook()不要自己new，Book对象了，使用我提前在数据库中取出的对象（hahaMyModelAttribute()方法做的事情）
 *             也就是，在目标方法的入参处，使用@ModelAttribute注解
 *
 * 原理：
 *      1）、hahaMyModelAttribute()，标注@ModelAttribute，之后会最先执行，在此方法中，查询到数据库中的book对象，
 *          并存储在BindingAwareModelMap中
 *      2）、updateBook()，在入参处标注@ModelAttribute注解，告知SpringMVC，不要自己new对象，而是从BindingAwareModelMap中，获取已经存储的book对象
 *
 *      BindingAwareModelMap
 *          是一个单例对象、Map、Model、ModelMap、ModelAndView，全都是BindingAwareModelMap
 *          作用范围：
 *              标注@ModelAttribute的，先行方法
 *              目标方法updateBook()
 *              页面信息index.jsp，页面上也可以直接获取BindingAwareModelMap存储的数据，通过request域获取：src/main/webapp/index.jsp
 *
 * @author 伟哥
 * @date 2021-05-09 18:16
 */
@Controller
public class ModelAttributeTest {
    private Object mapIsOnce;

    /**
     * 处理updateBook请求
     *      使用Spring自动接收并封装数据
     * @return
     */
    @RequestMapping("/updateBook")
    public ModelAndView updateBook(@ModelAttribute("dateBook") Book book,
                                   Map<String, Object> map,
                                   HttpSession session,
                                   HttpServletRequest request,
                                   HttpServletResponse response,
                                   ModelAndView modelAndView,
                                   Model model){
        // 1、测试接收的数据
        System.out.println("提交过来的图书信息"+book);
        /*
        提交过来的图书信息book{bookId=100, bookName='null', author='吴承恩', price=99.98, stock=100, sales=123}
        ★★★问题出现了：bookName=null，这里是因为，前端页面不允许修改，图书的名字。但，我们为了简单实用了SpringMVC在入参处，传递了Book对象，
                SpringMVC为我们自动封装Book对象，没有传入的参数，就会变成null；但是接下来，dao调用了全字段更新的sql语句，
                把bookName=null提交到数据库，就会破坏数据，所以我们不能让这种事情发生
         */

        /*
         2、调用图书对象传输数据，这里省略，不影响
         bookDao.update(Book book)
         dao调用sql语句
         String sql = "updated bs_book set title=?,
                               author=?,price=?,sales=?,
                               stock=?,img_path=?,
                               where id=?";

         */

        ModelAndView success = new ModelAndView("success");

        // true
        System.out.println(mapIsOnce == map);
        return success;
    }

    /**
     * 注解@ModelAttribute
     *        使用位置：
     *            方法：这个方法就会提前于目标方法先运行
     *
     *      细节：
     *          1）、注解@ModelAttribute会将此方法返回值，也放置在 隐含模型中，key是，返回值类型的首字母小写
     *                  @ModelAttribute
     *                  public Book hahaMyModelAttribute(Map<String, Object> map){
     *                      Book book = new Book(100, "西游记", "吴承恩", 98.98, 100, 123);
     *                      return book；
     *                  }
     *
     *                  ↑，就会放置 <book,book>在隐含模型中
     *
     *          2）、注解@ModelAttribute的属性value，指定一个key，会将方法返回值，用以指定的key放入隐含模型
     *              @ModelAttribute("haha")
     *                   public Book hahaMyModelAttribute(Map<String, Object> map){
     *                       Book book = new Book(100, "西游记", "吴承恩", 98.98, 100, 123);
     *                       return book；
     *                   }
     *
     *                   ↑，就会放置 <haha，book>在隐含模型中
     *
     *          3）、入参不使用@ModelAttribute标注，默认也会去隐含模型中进行查找。查找使用默认的key是，参数类型的首字母小写
     *              @RequestMapping("/updateBook")
     *              public ModelAndView updateBook(Book book,){
     *
     *              }
     *
     *                  ↑，就在隐含模型中查找book
     */
    @ModelAttribute
    public void hahaMyModelAttribute(Map<String, Object> map){
        /*
        先于目标方法updateBook运行！！
        提交过来的图书信息book{bookId=100, bookName='null', author='吴承恩', price=99.98, stock=100, sales=123}
         */
        System.out.println("先于目标方法updateBook运行！！");

        /*
        这里，我们不直接连接，数据库，太麻烦，自己new一个，相当于连接数据库了

        使用set方法，设置参数，有点麻烦
        Book book = new Book();
        book.setBookName("西游记");
        book.setBookId(100);
        book.setAuthor("吴承恩");
        book.setPrice(98.98);
        book.setSales(123);
        book.setStock(100);

        使用 有参构造器，设置参数，方便快捷
        1、从数据库中获取，图书信息
         */
        Book book = new Book(100, "西游记", "吴承恩", 98.98, 100, 123);
        System.out.println("数据库中，查询到的图书信息是："+book);

        /*
         2、将这个book对象，保存起来，方便目标方法使用
            1）在方法的入参处，传递Map、Model、ModelMap
            2）将book对象，保存在Map、Model、ModelMap中
         */
        map.put("dateBook",book);
        System.out.println("ModelAttribute查询了图书，并保存起来！：ModelAttribute使用的类型："+map.getClass());
        mapIsOnce = map;


    }
}
